昨天本喵遭受這突如其來的暴擊後,重新審視了一遍自己的程式碼,但並沒有發現與重構前有什麼重大不同的部分,除了一點外,就是在 BLE ISR 裡呼叫 micropython.schedule() 來處理 _IRQ_GATTS_READ_REQUEST!
為了確認問題點,將讀取回應改為如下:
_counter = 0
_rsp = bytearray(1)
def _send_read_rsp(value_handle):
global _counter
_counter += 1
_rsp[0] = _counter & 0xFF
# 將要回覆的讀取要求寫入 characteristic 裡
ble.stack.gatts_write(value_handle, _rsp)
common.logger.write(f"Send read response: {_rsp}")
重新執行測試,發現原來 nRF Connect app 每次讀取到的值,都是更新 IDD Features 前的值:
讀取次序 | IDD Features 當前內容 | nRF Connect App 讀到的值 |
---|---|---|
0 | None | N/A |
1 | 1 | None |
2 | 2 | 1 |
3 | 3 | 2 |
這是怎麼回事呢?若去察看 _ble_isr() 對 _IRQ_GATTS_READ_REQUEST 的處理,很快就會了解發生了什麼事:
def _ble_isr(self, event, data):
if event == _IRQ_GATTS_READ_REQUEST:
conn_handle, value_handle = data
micropython.schedule(_send_read_rsp, value_handle)
def _send_read_rsp(value_handle):
# 將要回覆的讀取要求寫入 characteristic 裡
ble.stack.gatts_write(value_handle, _rsp)
咱們的流程是:
問題點在於更新 IDD Features 是在步驟 4,而 MicroPython 是在步驟 2 結束前,就已讀取 IDD Features 的值,然後安排送出。
咱們可以佐以 MicroPython v1.25.0 的原始碼來驗證此猜想。
mp_int_t mp_bluetooth_gatts_on_read_request(uint16_t conn_handle, uint16_t value_handle) {
mp_int_t args[] = {conn_handle, value_handle};
mp_obj_t result = invoke_irq_handler(
MP_BLUETOOTH_IRQ_GATTS_READ_REQUEST,
args,
2,
0,
NULL_ADDR,
NULL_UUID,
NULL_DATA,
NULL_DATA_LEN,
0
);
// Return non-zero from IRQ handler to fail the read.
mp_int_t ret = 0;
mp_obj_get_int_maybe(result, &ret);
return ret;
}
invoke_irq_handler() 的工作,就是將 BLE 中斷事件,傳給在 bluetooth.BLE.irq() 註冊的 ISR,等 ISR 處理完後,返回 ISR 的回傳值。
而哪個函數會呼叫 mp_bluetooth_gatts_on_read_request() 呢?就是 characteristic_access_cb()。
為了不干擾閱讀,本喵只列出相關部分。
static int characteristic_access_cb(uint16_t conn_handle, uint16_t value_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
mp_bluetooth_gatts_db_entry_t *entry;
switch (ctxt->op) {
case BLE_GATT_ACCESS_OP_READ_CHR:
case BLE_GATT_ACCESS_OP_READ_DSC: {
DEBUG_printf("write for %d %d (op=%d)\n", conn_handle, value_handle, ctxt->op);
// Allow Python code to override (by using gatts_write), or deny (by returning false) the read.
// Note this will be a no-op if the ringbuffer implementation is being used (i.e. the stack isn't
// run in the scheduler). The ringbuffer is not used on STM32 and Unix-H4 only.
int req = mp_bluetooth_gatts_on_read_request(conn_handle, value_handle);
if (req) {
return req;
}
entry = mp_bluetooth_gatts_db_lookup(MP_STATE_PORT(bluetooth_nimble_root_pointers)->gatts_db, value_handle);
if (!entry) {
return BLE_ATT_ERR_ATTR_NOT_FOUND;
}
if (os_mbuf_append(ctxt->om, entry->data, entry->data_len)) {
return BLE_ATT_ERR_INSUFFICIENT_RES;
}
return 0;
}
}
return BLE_ATT_ERR_UNLIKELY;
}
可以看到,若 mp_bluetooth_gatts_on_read_request() 回傳 0,那麼就會由 mp_bluetooth_gatts_db_lookup() 取得與 value_handle 相關聯的 characteristic 的值,然後用 os_mbuf_append() 送出。
由以上分析,可以知道若要回應讀取事件,必須在離開 ISR 前,便將資料以 bluetooth.BLE.gatts_write() 寫進 characteristic:
gatts_write(value_handle: int, data: bytes)
可是有幾個要求:
而 gatts_write() 有個限制:
不支援寫入部分資料。若要傳送部分資料,須對資料進行切片,而這會造成記憶體分配。
對於要求 2,至少有 2 種方案:
為了解決「gatts_write() 的限制」,但又不違反「ISR 內不能配置記憶體」,有個很簡單的方法:
使用 memoryview。
memoryview 就像對底層記憶體的一個觀看窗口,對它進行切片時,不會造成切片部分被複製到一新的記憶體區域。雖然還是需要創建 memoryview 物件的記憶體,但若連這都禁止,那本喵還真不知道要怎麼在 MicroPython 平台處理中斷了。
於是,咱們可以這樣更新 _ble_isr() 和 _send_read_rsp():
def _ble_isr(self, event, data):
if event == _IRQ_GATTS_READ_REQUEST:
conn_handle, value_handle = data
_send_read_rsp(value_handle)
_counter = 0
_rsp = bytearray(20)
_rsp_mv = memoryview(_rsp)
def _send_read_rsp(value_handle):
global _counter
_counter += 1
_rsp_mv[0] = _counter & 0xFF
# 將要回覆的讀取要求寫入 characteristic 裡
ble.stack.gatts_write(value_handle, _rsp_mv[:1])
總算結束 BLE 的封裝了
本來預計只用一天來寫,沒想卻寫了三天 ...
(。ŏ_ŏ)